5.01. Node.js
Node.js
Мы уже не раз отмечали, что JS работает как полноценное backend-приложение, и как раз благодаря Node.
Что такое Node.js
Node.js — это программная платформа, позволяющая выполнять JavaScript-код вне среды веб-браузера, преимущественно на стороне сервера, но также и в любых других контекстах, где требуется выполнение автономных программных модулей: скриптов обработки данных, инструментов сборки, утилит командной строки, агентов мониторинга, микросервисов и даже настольных приложений. По своей сути Node.js — это выполнимая среда, объединяющая интерпретатор JavaScript на основе движка V8 от Google и набор системных библиотек, обеспечивающих доступ к ресурсам операционной системы.
Ключевое свойство Node.js — это его способность эффективно обрабатывать большое количество одновременных сетевых подключений при минимальных затратах ресурсов, что достигается за счёт архитектурных решений, заложенных в его ядро. Node.js не был задуман как инструмент для тяжелых вычислений — напротив, он оптимизирован под задачи, связанные с вводом-выводом и асинхронным взаимодействием. Его появление ознаменовало собой переход JavaScript из разряда «языка для анимации кнопок» в полноценный инструмент для построения сложных распределённых систем.
Важно подчеркнуть: Node.js — это единая экосистема, объединяющая выполнение кода и управление зависимостями. Без интеграции с npm (Node Package Manager), который по умолчанию поставляется вместе с дистрибутивом, невозможно представить практическое использование Node.js в реальных проектах. Это делает его не просто runtime, но и основой целой инфраструктурной культуры разработки — от написания локального скрипта до развёртывания глобального сервиса.
История Node.js
Node.js был создан в 2009 году Райаном Далем (Ryan Dahl), инженером, разочаровавшимся в архитектурных ограничениях существовавших на тот момент решений для веб-серверов. В своём знаменитом докладе «Node.js is cancer» (что не следует понимать буквально — это провокационный заголовок для критики текущего состояния серверной разработки) Даль продемонстрировал, как традиционные модели обработки запросов — потоковая и процессорная — демонстрируют неоправданно высокое потребление памяти и сложность масштабирования даже при умеренной нагрузке. Он предложил альтернативу: использование событийной модели с неблокирующими операциями ввода-вывода, основанной на цикле событий (event loop), — подход, давно применявшийся в таких системах, как Nginx или в библиотеках типа libevent, но никогда не реализованный на JavaScript.
Платформа была построена на основе движка V8 — компилятора и JIT-оптимизатора, разработанного Google для Chrome. Выбор V8 объяснялся его высокой производительностью и открытостью кодовой базы, возможностью внедрения собственных привязок (bindings) к системным API и стабильностью ABI, что давало возможность интегрировать C++-код напрямую в среду выполнения.
В 2010 году была выпущена первая стабильная версия 0.2.0. В последующие годы Node.js стремительно развивался за счёт роста сообщества, появления npm (созданного Исааком Шлютером в 2010 году), и постепенного внедрения в промышленную эксплуатацию — сначала в стартапах (например, LinkedIn, PayPal), затем и в крупных корпорациях. В 2014 году раскол в разработке, вызванный разногласиями в подходах к эволюции платформы, привёл к появлению форка io.js, который в 2015 году вернулся в основное русло под эгидой Node.js Foundation — нейтральной организации, позже поглощённой OpenJS Foundation. С этого момента циклы выпуска версий стали предсказуемыми и регулярными:
- LTS-версии (Long Term Support) — поддерживаются 30+ месяцев, рекомендуются для production;
- Current-версии — содержат новейшие возможности и выходят каждые шесть месяцев, но поддерживаются лишь до выхода следующей LTS.
Эта предсказуемость стала критически важной для корпоративного внедрения Node.js и легла в основу его репутации как зрелой, надёжной платформы.
Архитектура Node.js: как он работает
Сердце Node.js — цикл событий (event loop), реализованный на основе библиотеки libuv. Эта библиотека, написанная на C, обеспечивает кроссплатформенную абстракцию для системных вызовов: таймеров, сетевых сокетов, файловых операций, потоков ввода-вывода. В отличие от традиционных серверных платформ, где каждый запрос обрабатывается в отдельном потоке или процессе, Node.js использует однопоточную модель выполнения JavaScript-кода, но при этом делегирует тяжелые I/O-операции в фоновые потоки через пул потоков libuv, сохраняя при этом управление в основном потоке.
Рассмотрим упрощённый жизненный цикл операции:
- JavaScript-код вызывает, например,
fs.readFile()— асинхронное чтение файла. - Этот вызов передаётся в
libuv, где создаётся задача и ставится в очередь на выполнение в пуле потоков. - Основной поток Node.js не ждёт завершения операции, а продолжает выполнять последующие инструкции.
- По завершении чтения файловой системы,
libuvпомещает колбэк, связанный с этой операцией, в очередь событий (callback queue). - Цикл событий, завершив текущую фазу (например, обработку тиков, таймеров, IO-событий), извлекает колбэк из очереди и передаёт его на выполнение в контексте V8.
Таким образом, основной поток остаётся свободным для приёма новых запросов, что обеспечивает высокую пропускную способность даже при ограниченных вычислительных ресурсах. Это — неблокирующий ввод-вывод в действии.
Важно понимать: Node.js не является полностью однопоточным. Сами операции ввода-вывода, криптографические функции, zlib-сжатие и другие ресурсоёмкие задачи выполняются в фоновых потоках. Блокировка происходит только в том случае, если разработчик намеренно запускает синхронную операцию (fs.readFileSync, child_process.execSync) или пишет вычислительно тяжёлый JS-код без прерываний. Для таких случаев предусмотрены механизмы вроде worker_threads, позволяющие распараллеливать выполнение на уровне JavaScript.
Основные принципы и функции
Node.js строится на нескольких ключевых принципах, определяющих стиль и культуру разработки:
- Единый язык сквозь стек: возможность использовать JavaScript как на клиенте, так и на сервере, упрощает передачу знаний, логики и даже кода между уровнями приложения.
- Минимализм ядра: ядро Node.js включает только базовые возможности — файловая система, сеть, потоки, шифрование, модули. Всё остальное выносится в пользовательские пакеты. Это обеспечивает стабильность и предотвращает «разбухание» платформы.
- Модульность и композиция: приложения строятся из маленьких, узкоспециализированных модулей, каждый из которых решает одну задачу и решает её хорошо. Такой подход способствует переиспользованию и тестируемости.
- Конвенция над конфигурацией: хотя Node.js не навязывает структуру проекта, сообщество выработало устойчивые практики (например,
package.jsonв корне,node_modules/как хранилище зависимостей), что упрощает взаимодействие между разработчиками и инструментами. - Неблокирующий I/O по умолчанию: почти все системные API предоставляются в асинхронном виде. Синхронные версии помечены явно (
Sync) и используются только в исключительных случаях (например, инициализация приложения при запуске).
Функционально Node.js предоставляет доступ к следующим ключевым областям:
- Файловая система (
fs,path,os): чтение, запись, мониторинг, навигация по каталогам; - Сетевые протоколы (
http,https,net,dgram): создание HTTP-серверов и клиентов, TCP/UDP-сокетов; - Потоки (
stream,readline): эффективная обработка данных большого объёма без загрузки всей информации в память; - Дочерние процессы (
child_process,cluster): порождение и управление внешними программами, распараллеливание на уровне процессов; - Шифрование и безопасность (
crypto,tls): хэширование, шифрование, цифровые подписи, TLS-соединения; - Системная информация (
os,process,v8): доступ к параметрам ОС, переменным окружения, состоянию движка.
Установка Node.js
Для начала работы с Node.js требуется установить сам runtime и сопутствующий инструментарий — в первую очередь, npm.
Способ 1: Официальный сайт nodejs.org
На https://nodejs.org доступны две основные сборки:
- LTS (Long Term Support) — рекомендуется для большинства проектов, особенно production-сред. Гарантированная поддержка, стабильность API, обратная совместимость.
- Current — последняя функционально полная версия. Подходит для экспериментов, изучения новых возможностей, но не рекомендуется для развёртывания в промышленной эксплуатации.
Установка производится стандартным образом:
- Windows: загружается
.msi-установщик, который автоматически добавляетnodeиnpmв PATH и создаёт ярлыки. - macOS: предоставляет
.pkg-пакет, также настраивающий окружение в/usr/local/bin. - Linux (Debian/Ubuntu, RHEL/CentOS): доступны
.debи.rpm-пакеты, а также рекомендуемый способ — установка через менеджер пакетов с официального репозитория NodeSource.
После установки проверьте корректность:
node --version
npm --version
Важно: установка через системные пакетные менеджеры (apt install nodejs) зачастую даёт устаревшие версии и может не включать npm, либо требовать отдельной установки (apt install npm). Поэтому предпочтителен прямой пакет от NodeSource.
Способ 2: Node Version Manager (nvm)
Для разработчиков, работающих с несколькими проектами, каждый из которых может требовать разные версии Node.js, критически важен инструмент nvm (Node Version Manager). Это bash/zsh-скрипт, позволяющий:
- устанавливать несколько версий Node.js параллельно;
- мгновенно переключаться между ними;
- задавать версию по умолчанию;
- указывать требуемую версию в
.nvmrcфайле проекта.
Установка nvm (на macOS/Linux):
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
# или через wget
wget -qO- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
После перезапуска терминала или выполнения source ~/.bashrc (или ~/.zshrc) доступны команды:
nvm install --lts # установить последнюю LTS-версию
nvm install 18.18.0 # установить конкретную версию
nvm use --lts # использовать LTS по умолчанию
nvm use 18.18.0 # переключиться на конкретную версию
nvm list # показать установленные версии
nvm alias default 18.18.0 # сделать версию умолчательной при новом сеансе
nvm хранит каждую версию Node.js в изолированной директории (~/.nvm/versions/node/...), не затрагивая системные пути, что исключает конфликты.
На Windows существует аналог — nvm-windows (https://github.com/coreybutler/nvm-windows), хотя его использование менее универсально из-за различий в обработке переменных окружения.
Глобальные объекты в Node.js
В отличие от браузерного JavaScript, где глобальным контекстом служит объект window, в Node.js используется объект global. Однако в повседневной практике разработчик редко обращается к нему напрямую — большинство часто используемых сущностей доступны автоматически, без явного импорта или префикса. Это так называемые глобальные переменные и функции среды выполнения.
К числу наиболее важных относятся:
-
process— объект, предоставляющий информацию о текущем процессе Node.js и позволяющий управлять им. Черезprocessможно:- получить аргументы командной строки (
process.argv); - прочитать и задать переменные окружения (
process.env); - определить текущую рабочую директорию (
process.cwd()); - завершить выполнение (
process.exit(code)); - отловить сигналы ОС, такие как
SIGINT(Ctrl+C) илиSIGTERM, черезprocess.on('SIGINT', …); - получить метрики потребления памяти и CPU (
process.memoryUsage(),process.cpuUsage()).
process— один из центральных элементов интеграции Node.js с операционной системой и ключевой инструмент для написания устойчивых, управляемых приложений. - получить аргументы командной строки (
-
console— объект для вывода отладочной и диагностической информации. Поддерживает привычные методы:console.log(),console.warn(),console.error(), а такжеconsole.time()/console.timeEnd()для замера длительности операций,console.table()для табличного вывода объектов и массивов. Важно:console.log()направляет данные вstdout, аconsole.error()— вstderr, что критично при перенаправлении потоков в логи или CI/CD-системы. -
__dirnameи__filename— строки, содержащие абсолютный путь к текущей директории и к исполняемому файлу соответственно. Эти переменные доступны только в CommonJS-модулях и исчезают при переходе на ESM (где их заменяетimport.meta.urlиURL-API). -
Buffer— глобальный класс для работы с бинарными данными. Позволяет эффективно оперировать сырыми байтами — от чтения файлов до обмена по сети, шифрования и работы с изображениями. В отличие отArrayBufferв браузере,Buffer— это расширение, специфичное для Node.js, хотя в современных версиях он реализован поверх стандартногоUint8Array. -
setTimeout,setInterval,setImmediate— таймеры, унаследованные от браузерного API, но с важными отличиями в порядке выполнения в цикле событий. Например,setImmediate()гарантированно выполнится после завершения текущей фазы I/O, но до следующего тикаsetTimeout(fn, 0).
Эти объекты формируют «скелет» среды выполнения и делают возможным взаимодействие с внешним миром без необходимости подключать дополнительные модули.
Модульная система: CommonJS и ECMAScript Modules
Node.js исторически использовал систему модулей CommonJS (CJS), разработанную для server-side JavaScript ещё до появления стандарта ECMAScript Modules (ESM). Эта система базируется на двух ключевых конструкциях:
require(id)— синхронная функция, загружающая модуль по указанному пути или имени;module.exports— объект, экспортируемый из модуля; часто используется сокращениеexports, которое по умолчанию ссылается наmodule.exports.
Пример CJS-модуля:
// math.js
const add = (a, b) => a + b;
const PI = 3.14159;
module.exports = { add, PI };
// или по отдельности:
// exports.add = add;
// exports.PI = PI;
// app.js
const math = require('./math');
console.log(math.add(2, 3)); // 5
Важные свойства CommonJS:
- Модули загружаются синхронно (блокируя выполнение до полной загрузки);
- Кешируются после первого импорта: повторные вызовы
require()возвращают ссылку на уже загруженный объект; - Поддерживают циклические зависимости (хотя это считается плохой практикой);
- Расширение
.jsможет быть опущено в пути; также поддерживаются.jsonи.node(нативные аддоны).
С версии 12.20.0 и 14.13.0 Node.js официально поддерживает ES Modules — стандартный механизм модулей языка JavaScript, основанный на статических конструкциях import и export. Для включения ESM необходимо либо:
- использовать расширение
.mjs; - или указать
"type": "module"вpackage.json.
Пример ESM-модуля:
// math.mjs (или math.js при "type": "module")
export const add = (a, b) => a + b;
export const PI = 3.14159;
export default class Calculator { /* … */ }
// app.mjs
import { add, PI } from './math.js';
import Calculator from './math.js';
console.log(add(2, 3));
Ключевые отличия ESM от CommonJS:
- Импорт и экспорт — статические, разрешаются на этапе парсинга, до выполнения кода;
- Поддержка именованных и default-экспортов в едином синтаксисе;
- Возможность импорта JSON-файлов с флагом
--experimental-json-modules(устарело в пользуimport fs from 'fs'; const data = JSON.parse(fs.readFileSync(...))); - Динамический импорт через
import()— асинхронная функция, возвращающаяPromise.
Node.js позволяет смешивать CJS и ESM, но с ограничениями:
- ESM-модуль не может использовать
require()напрямую; - CJS-модуль не может использовать
import, но может вызыватьimport()асинхронно; - При импорте CJS-модуля из ESM он автоматически оборачивается в объект с
default-свойством — например,import pkg from 'lodash'вместоconst pkg = require('lodash').
Выбор между CJS и ESM сегодня — это выбор экосистемы и инструментария. Большинство legacy-проектов и многих библиотек по-прежнему используют CommonJS, но новые разработки всё чаще переходят на ESM, особенно в связке с bundler’ами (Vite, esbuild, Rollup).
Хранение модулей и работа npm
Когда вы устанавливаете пакет через npm install, он помещается в локальную директорию node_modules, расположенную в корне проекта. Эта директория иерархическая, но не древовидная в классическом смысле: npm (начиная с версии 3) применяет плоскую структуру, где зависимости поднимаются как можно выше, чтобы избежать дублирования. Например:
project/
├── node_modules/
│ ├── express/
│ ├── lodash/
│ └── debug/ ← общая зависимость express и других пакетов
└── package.json
Если два пакета требуют разные несовместимые версии одной зависимости, npm создаёт вложенную папку node_modules внутри папки «конфликтующего» пакета — тем самым изолируя версии.
Механизм разрешения модулей (require('express')) работает по следующему алгоритму:
- Если путь начинается с
/,./или../— поиск ведётся относительно текущего файла (локальный модуль); - Иначе — поиск в
node_modules, начиная от текущей директории и поднимаясь вверх по дереву каталогов до корня файловой системы; - Если модуль не найден — проверяется наличие в глобальных путях (например, при установке через
npm install -g), хотя их использование в проектах не рекомендуется.
npm как экосистема
npmjs.com — это публичный реестр пакетов, являющийся крупнейшим в мире хранилищем открытого программного обеспечения. По состоянию на 2025 год в нём более двух миллионов пакетов, которыми пользуются свыше 17 миллионов разработчиков. Реестр бесплатен для публикации и установки открытых пакетов. Для корпоративного использования доступны:
- npm Pro / Teams / Orgs — приватные пакеты, расширенные метрики, управление доступом;
- npm Enterprise — локальный прокси-реестр с кэшированием и политиками безопасности (например, блокировка пакетов с уязвимостями).
npm также предоставляет инструменты аудита (npm audit), генерации лицензионных отчётов (npm license), автоматического обновления (npm outdated, npm update), и интеграции с CI/CD.
Работа с файловой системой, сетью и процессами
Node.js предоставляет стандартные встроенные модули, инкапсулирующие низкоуровневые системные вызовы.
Файловая система (fs)
Модуль fs (file system) позволяет читать, писать, удалять, копировать и отслеживать изменения файлов и директорий. Поддерживает три режима работы:
- асинхронный с колбэками (
fs.readFile(path, cb)), - асинхронный с промисами (
import { readFile } from 'fs/promises'), - синхронный (
fs.readFileSync(path)— блокирует поток, рекомендуется только при инициализации).
Современный код предпочитает Promise-версию, так как она совместима с async/await:
import { readFile, writeFile } from 'fs/promises';
try {
const data = await readFile('config.json', 'utf8');
const config = JSON.parse(data);
await writeFile('backup.json', JSON.stringify(config, null, 2));
} catch (err) {
console.error('Ошибка работы с файлом:', err);
}
Модуль path дополняет fs: он нормализует пути (path.join, path.resolve), извлекает расширения (path.extname), имена (path.basename), и обеспечивает кроссплатформенную совместимость (разделители / vs \).
Сеть (http, net, dgram)
Модуль http позволяет создавать полноценные HTTP-серверы и клиенты без внешних зависимостей:
import http from 'http';
const server = http.createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('Привет из Node.js!\n');
});
server.listen(3000, () => {
console.log('Сервер запущен на http://localhost:3000');
});
Для HTTPS используется https (с нужным SSL-сертификатом). Модуль net даёт доступ к TCP-сокетам, dgram — к UDP. Все они построены на одном принципе: событийная модель с EventEmitter.
Процессы (child_process, cluster)
Для запуска внешних команд используется child_process:
import { exec, spawn } from 'child_process';
// exec — удобен для коротких команд, возвращает весь вывод
exec('git --version', (err, stdout) => {
if (err) throw err;
console.log('Версия Git:', stdout.trim());
});
// spawn — потоковый, подходит для длительных/объёмных операций
const ls = spawn('ls', ['-lh', '/usr']);
ls.stdout.on('data', (data) => console.log(data.toString()));
Для горизонтального масштабирования на многоядерных машинах — cluster, который создаёт «мастер-воркер» архитектуру: один мастер-процесс распределяет входящие соединения между рабочими процессами.
Основные команды CLI
Запуск Node.js
-
node— без аргументов запускает интерактивную REPL-сессию (Read-Eval-Print Loop), где можно выполнять JavaScript на лету:$ node
> const x = 1 + 2;
undefined
> console.log(x);
3
undefined
> .exit -
node <файл.js>— запускает скрипт. -
node --watch <файл.js>(с версии 18.11+) — перезапускает скрипт при изменении файла (аналог nodemon для простых случаев).
Управление проектом через npm
После npm init (или npm init -y для быстрой инициализации) создаётся package.json — манифест проекта, содержащий:
- имя и версию;
- точку входа (
"main": "index.js"); - скрипты (
"scripts": { "start": "node server.js" }); - зависимости (
"dependencies","devDependencies"); - метаинформацию (лицензия, репозиторий, авторы).
Основные команды:
| Команда | Назначение |
|---|---|
npm install | Установить все зависимости из package.json |
npm install lodash | Установить пакет и добавить в dependencies |
npm install --save-dev jest | Установить в devDependencies (тесты, сборка) |
npm install -g typescript | Установить глобально (доступно везде, но не в проекте) |
npm uninstall <pkg> | Удалить пакет и обновить package.json |
npm run <script> | Выполнить пользовательский скрипт: npm run build → node build.js |
npm start | Сокращение для npm run start (специальный скрипт) |
npm test | Выполняет npm run test |
npm outdated | Показать устаревшие зависимости |
npm audit | Проверить зависимости на наличие известных уязвимостей |
Скрипты в package.json могут включать цепочки команд, переменные окружения, перенаправление потоков:
{
"scripts": {
"dev": "NODE_ENV=development node server.js",
"build": "rimraf dist && tsc",
"postinstall": "npx patch-package"
}
}
Обратите внимание: на Windows для установки переменных окружения используется set NODE_ENV=development && node server.js или кроссплатформенный пакет cross-env.
Практика: развёртывание локального Node.js-приложения
Рассмотрим пошагово, как создать и запустить минимальное приложение.
1. Установка Node.js и проверка
node --version # → v18.18.2 (LTS)
npm --version # → 9.8.1
2. Создание проекта
mkdir my-app && cd my-app
npm init -y
Файл package.json создан.
3. Написание сервера
Создадим server.js:
import http from 'http';
const hostname = '127.0.0.1';
const port = 3000;
const server = http.createServer((req, res) => {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
res.end('✅ Локальный сервер работает!\n');
});
server.listen(port, hostname, () => {
console.log(`Сервер запущен на http://${hostname}:${port}/`);
});
4. Запуск
node server.js
# → Сервер запущен на http://127.0.0.1:3000/
Открываем браузер или curl http://localhost:3000 — видим ответ.
5. Добавление скрипта в package.json
В package.json добавим:
{
"scripts": {
"start": "node server.js",
"dev": "node --watch server.js"
}
}
Теперь можно:
npm start # запуск
npm run dev # с перезагрузкой при изменении
6. Установка и использование внешнего пакета
Установим, например, uuid для генерации идентификаторов:
npm install uuid
Обновим server.js:
import http from 'http';
import { v4 as uuidv4 } from 'uuid';
const server = http.createServer((req, res) => {
const id = uuidv4();
res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' });
res.end(`Запрос #${id}\n`);
});
server.listen(3000, () => {
console.log('Сервер с UUID запущен');
});
После перезапуска каждый запрос будет возвращать уникальный идентификатор.
Модуль http: ядро серверной логики
Модуль http — один из самых фундаментальных в Node.js. Он предоставляет низкоуровневый, но полностью функциональный API для создания HTTP-серверов и клиентов, без привязки к каким-либо фреймворкам. Понимание его устройства необходимо для написания минималистичных сервисов и для осознанной работы с Express, Fastify и другими надстройками.
Сервер: от соединения до ответа
Когда вызывается http.createServer(callback), создаётся экземпляр http.Server, унаследованный от EventEmitter. При подключении клиента генерируется событие 'request', которому передаются два объекта:
-
req(IncomingMessage) — читаемый поток (ReadableStream), представляющий входящий HTTP-запрос. Содержит:- метаинформацию:
req.method,req.url,req.httpVersion; - заголовки:
req.headers, в нормализованном нижнем регистре; - данные тела — через прослушивание событий
'data'и'end', либо через промис-обёртки (например,stream.pipelineили сторонние утилиты вродеget-stream); - свойство
req.socket— ссылка на TCP-сокет, что позволяет, например, определить IP-адрес (req.socket.remoteAddress).
- метаинформацию:
-
res(ServerResponse) — записываемый поток (WritableStream), через который формируется ответ. Основные методы:res.writeHead(statusCode, [headers])— отправка стартовой строки и заголовков (можно вызывать до первогоwrite()илиend());res.write(chunk, [encoding])— отправка части тела ответа (буфер или строка);res.end([chunk], [encoding])— завершение ответа, необязательно с финальной порцией данных;res.setHeader(name, value),res.getHeader(name),res.removeHeader(name)— управление заголовками до отправки;res.statusCode,res.statusMessage— установка кода и текстового поясняющего статуса.
Обратите внимание: отправка заголовков происходит при первом вызове write() или end(). До этого момента их можно изменять. Если разработчик забывает явно вызвать writeHead(), Node.js сделает это автоматически при end(), используя текущие значения statusCode и headers.
Потоковая обработка
Поскольку req — это Readable, а res — Writable, весь HTTP-трафик в Node.js по умолчанию потоковый. Это означает, что данные обрабатываются по частям, без загрузки всего тела в память — критически важно для загрузки файлов, проксирования, streaming-медиа.
Пример: проксирование запроса через http.request() (клиентская часть модуля):
import http from 'http';
const proxy = http.createServer((clientReq, clientRes) => {
const options = {
hostname: 'api.example.com',
port: 80,
path: clientReq.url,
method: clientReq.method,
headers: clientReq.headers
};
const proxyReq = http.request(options, (proxyRes) => {
clientRes.writeHead(proxyRes.statusCode, proxyRes.headers);
proxyRes.pipe(clientRes); // потоковая передача без буферизации
});
clientReq.pipe(proxyReq); // потоковая передача тела от клиента к прокси
});
proxy.listen(8080);
Здесь используется метод .pipe(), унаследованный от stream — универсальный механизм связывания потоков чтения и записи.
Обработка ошибок и таймауты
Сервер должен обрабатывать:
- ошибки соединения (
server.on('clientError', (err, socket) => …)), - таймауты (
req.setTimeout(ms, callback)), - необработанные исключения (через
process.on('uncaughtException'), но с осторожностью — это аварийный режим).
Рекомендуется также явно закрывать сокеты при ошибках, чтобы избежать «висячих» соединений.
Фреймворки: Express, Fastify, NestJS
Хотя «голый» http достаточен для простых задач, реальные приложения требуют маршрутизации, middleware, валидации, сериализации, инъекции зависимостей. Для этого существуют фреймворки — разного уровня абстракции и архитектурной выразительности.
Express.js — минимализм и гибкость
Express (https://expressjs.com) — самый старый и распространённый фреймворк для Node.js, созданный в 2010 году. Его ключевые черты:
- Простота ядра: около 2000 строк кода; всё остальное — через middleware.
- Middleware-цепочка: функции
(req, res, next) => {…}, выполняемые последовательно. Каждый middleware может:- модифицировать
req/res; - завершить ответ (
res.send()); - передать управление дальше (
next()), либо с ошибкой (next(err)).
- модифицировать
- Декларативная маршрутизация:
app.get('/users/:id', (req, res) => {
res.json({ id: req.params.id });
}); - Экосистема: тысячи middleware (
cors,helmet,morgan,body-parser), адаптеров (для сессий, шаблонизаторов), ORM-интеграций.
Выраженный unopinionated подход: Express не навязывает структуру проекта, архитектурный стиль или способ тестирования. Это даёт свободу, но требует дисциплины — легко допустить хаос в кодовой базе при масштабировании.
Fastify — производительность и схемы
Fastify (https://fastify.io) — современный фреймворк, построенный на идее «высокой скорости без жертв в удобстве». Его отличия:
-
Встроенная валидация и сериализация через JSON Schema:
fastify.post('/user', {
schema: {
body: { type: 'object', properties: { name: { type: 'string' } } },
response: { 200: { type: 'object', properties: { id: { type: 'number' } } } }
}
}, async (request, reply) => {
return { id: 1 };
});Схемы компилируются в высокопроизводительные функции валидации (с использованием
ajv), что устраняет необходимость вручную писать проверки. -
Плагинная архитектура с изоляцией контекста: каждый плагин получает собственный экземпляр
fastify, что обеспечивает инкапсуляцию и предотвращает конфликты имён. -
Автоматическая документация: интеграция с Swagger/OpenAPI «из коробки».
-
Производительность: на бенчмарках (например, TechEmpower) Fastify часто в 2–3 раза быстрее Express при эквивалентной функциональности, благодаря оптимизациям в маршрутизации и отсутствию лишних абстракций.
Fastify подходит для микросервисов, API-шлюзов, high-load систем, где важны скорость и строгая типизация.
NestJS — enterprise-подход и TypeScript-first
NestJS (https://nestjs.com) — фреймворк, вдохновлённый Angular и Spring. Он не конкурирует напрямую с Express/Fastify — скорее, надстраивается над ними (по умолчанию использует Express, но поддерживает Fastify как адаптер).
Основные принципы:
-
Архитектура, основанная на модулях, контроллерах и провайдерах:
@Module()— декларирует зависимости;@Controller()— обрабатывает HTTP-запросы;@Injectable()— сервисы с логикой бизнес-процессов;@Inject()— внедрение зависимостей (DI).
-
Полная поддержка TypeScript «из коробки»: типизация, метаданные, строгая проверка на этапе компиляции.
-
Расширяемость:
- Guards (авторизация),
- Interceptors (логирование, кэширование),
- Pipes (валидация и преобразование),
- Exception filters (централизованная обработка ошибок).
Пример контроллера:
@Controller('cats')
export class CatsController {
constructor(private readonly catsService: CatsService) {}
@Get()
findAll(): Cat[] {
return this.catsService.findAll();
}
}
NestJS нацелен на крупные проекты с командной разработкой, где важны maintainability, тестируемость и соответствие enterprise-стандартам. Стоимость — более высокий порог вхождения и некоторая избыточность для простых задач.
| Критерий | Express | Fastify | NestJS |
|---|---|---|---|
| Скорость | ⚖️ средняя | 🚀 высокая | ⚖️ (зависит от адаптера) |
| Объём кода | 🟢 минимум | 🟢 компактно | 🔴 много декораторов/файлов |
| Гибкость | 🟢 максимальная | 🟢 высокая | 🟡 контролируемая |
| Поддержка TypeScript | 🟡 через @types | 🟢 встроенная | 🟢 native |
| Подход | императивный | декларативный + схемы | декларативный + DI |
| Когда использовать | MVP, легаси, обучение | API, микросервисы, high-load | enterprise, команды, сложные домены |
Прочие важные особенности Node.js
Обработка ошибок
Node.js использует два основных механизма:
- Исключения (
throw) — для синхронного кода; - Колбэки с
errпервым аргументом или отклонённые промисы — для асинхронного.
Ключевые практики:
- Всегда обрабатывайте ошибки в
async-функциях черезtry/catch; - Не игнорируйте
unhandledRejection:process.on('unhandledRejection', (reason, promise) => {
console.error('Необработанный rejection:', reason);
// логирование, graceful shutdown
}); - Используйте кастомные классы ошибок с
nameиcodeдля диагностики.
Graceful shutdown
При получении сигнала завершения (SIGTERM, SIGINT) сервер должен:
- Перестать принимать новые соединения;
- Завершить обработку текущих запросов;
- Освободить ресурсы (соединения с БД, таймеры, сокеты);
- Вызвать
process.exit().
Пример:
let shuttingDown = false;
const server = http.createServer(handler);
process.on('SIGTERM', shutdown);
process.on('SIGINT', shutdown);
function shutdown() {
if (shuttingDown) return;
shuttingDown = true;
console.log('Начинаем graceful shutdown…');
server.close(() => {
console.log('HTTP-сервер остановлен');
// здесь — отключение от БД, закрытие пулов и т.п.
process.exit(0);
});
// Принудительный выход через 10 сек, если не завершилось
setTimeout(() => process.exit(1), 10_000);
}
Потоки (Streams)
Помимо http, потоки лежат в основе fs, zlib, crypto, child_process. Четыре типа:
Readable— источники данных;Writable— приёмники;Duplex— и то, и другое (например, TCP-сокет);Transform— преобразуют данные (например,zlib.createGzip()).
Потоки позволяют эффективно обрабатывать гигабайты данных с постоянным потреблением памяти.
Worker Threads
Для CPU-интенсивных задач (шифрование, расчёт хэшей, обработка изображений) Node.js предоставляет модуль worker_threads, реализующий модель shared-memory threads:
import { Worker, isMainThread, parentPort } from 'worker_threads';
if (isMainThread) {
const worker = new Worker(__filename, { workerData: 1000000 });
worker.on('message', (result) => console.log('Результат:', result));
} else {
const { workerData } = require('worker_threads');
let sum = 0;
for (let i = 0; i < workerData; i++) sum += i;
parentPort.postMessage(sum);
}
В отличие от child_process, worker threads дешевле в создании и позволяют передавать данные через SharedArrayBuffer.
Практикум: Построение REST API с Express и PostgreSQL
Цель — создать минимальное, но промышленно пригодное API для управления сущностью Task (идентификатор, заголовок, статус, дата создания). Акцент делается на архитектурные слои, обратную совместимость, безопасность и поддерживаемость.
1. Подготовка окружения
- Установите PostgreSQL (локально или в Docker):
docker run --name postgres -e POSTGRES_USER=app -e POSTGRES_PASSWORD=secret -e POSTGRES_DB=tasks -p 5432:5432 -d postgres:15 - Создайте проект:
mkdir tasks-api && cd tasks-api
npm init -y
npm install express pg
npm install --save-dev typescript ts-node @types/express @types/pg
npx tsc --init --outDir dist --rootDir src --target es2020 --module commonjs --esModuleInterop true
2. Архитектура: слои и разделение ответственности
Рекомендуемая структура:
src/
├── app.ts # точка входа: создаёт Express-приложение
├── server.ts # запуск сервера, graceful shutdown
├── config/ # конфигурация (переменные окружения, валидация)
│ └── index.ts
├── database/ # подключение к БД, пул соединений
│ └── client.ts
├── models/ # логика доступа к данным (Data Access Layer)
│ └── TaskModel.ts
├── controllers/ # обработка запросов, вызов моделей
│ └── TaskController.ts
├── routes/ # маршрутизация
│ └── taskRoutes.ts
└── middlewares/ # обработка ошибок, валидация, логирование
├── errorHandler.ts
└── validation.ts
Такой подход (инверсия зависимостей, separation of concerns) позволяет:
- легко заменить ORM или драйвер БД;
- тестировать слои изолированно;
- избежать «монолитных» файлов с сотнями строк.
3. Подключение к PostgreSQL
В src/database/client.ts:
import { Pool } from 'pg';
const pool = new Pool({
user: process.env.DB_USER || 'app',
host: process.env.DB_HOST || 'localhost',
database: process.env.DB_NAME || 'tasks',
password: process.env.DB_PASSWORD || 'secret',
port: Number(process.env.DB_PORT) || 5432,
});
export default pool;
Важно: никогда не храните учётные данные в коде. Используйте
.env-файлы только в разработке, а в production — секреты через переменные окружения платформы (Docker secrets, AWS Secrets Manager, и т.п.).
4. Модель: безопасный доступ к данным
В src/models/TaskModel.ts:
import pool from '../database/client';
export interface Task {
id: number;
title: string;
status: 'todo' | 'in_progress' | 'done';
created_at: Date;
}
export class TaskModel {
static async findAll(): Promise<Task[]> {
const result = await pool.query<Task>('SELECT * FROM tasks ORDER BY created_at DESC');
return result.rows;
}
static async create(title: string, status: Task['status'] = 'todo'): Promise<Task> {
// Защита от SQL-инъекций через параметризованные запросы
const result = await pool.query<Task>(
'INSERT INTO tasks (title, status) VALUES ($1, $2) RETURNING *',
[title, status]
);
return result.rows[0];
}
static async findById(id: number): Promise<Task | null> {
const result = await pool.query<Task>('SELECT * FROM tasks WHERE id = $1', [id]);
return result.rows[0] || null;
}
}
Предварительно создайте таблицу:
CREATE TABLE IF NOT EXISTS tasks (
id SERIAL PRIMARY KEY,
title TEXT NOT NULL,
status TEXT NOT NULL CHECK (status IN ('todo', 'in_progress', 'done')),
created_at TIMESTAMPTZ DEFAULT NOW()
);
5. Контроллер и маршруты
src/controllers/TaskController.ts:
import { Request, Response } from 'express';
import { Task, TaskModel } from '../models/TaskModel';
export class TaskController {
static async list(req: Request, res: Response) {
try {
const tasks = await TaskModel.findAll();
res.json(tasks);
} catch (err) {
res.status(500).json({ error: 'Ошибка при получении задач' });
}
}
static async create(req: Request, res: Response) {
const { title, status } = req.body;
if (!title || typeof title !== 'string') {
return res.status(400).json({ error: 'Поле "title" обязательно и должно быть строкой' });
}
try {
const task = await TaskModel.create(title, status);
res.status(201).json(task);
} catch (err) {
res.status(500).json({ error: 'Ошибка при создании задачи' });
}
}
}
src/routes/taskRoutes.ts:
import { Router } from 'express';
import { TaskController } from '../controllers/TaskController';
const router = Router();
router.get('/', TaskController.list);
router.post('/', TaskController.create);
export default router;
6. Сборка приложения
src/app.ts:
import express from 'express';
import taskRoutes from './routes/taskRoutes';
import { errorHandler } from './middlewares/errorHandler';
const app = express();
// Middleware
app.use(express.json({ limit: '10mb' })); // защита от перегрузки
// Маршруты
app.use('/api/tasks', taskRoutes);
// Централизованная обработка ошибок
app.use(errorHandler);
export default app;
src/server.ts:
import app from './app';
const PORT = process.env.PORT ? Number(process.env.PORT) : 3000;
const server = app.listen(PORT, () => {
console.log(`✅ REST API запущен на порту ${PORT}`);
});
// Graceful shutdown
process.on('SIGTERM', () => {
server.close(() => {
console.log('Сервер остановлен');
process.exit(0);
});
});
7. Запуск
npm install -g ts-node
npx ts-node src/server.ts
Проверка:
curl -X POST http://localhost:3000/api/tasks -H "Content-Type: application/json" -d '{"title":"Научиться Node.js"}'
curl http://localhost:3000/api/tasks
Расширения для production:
- Валидация входных данных через
zodилиjoi;- Логирование через
pinoилиwinston;- Миграции БД (
drizzle-kit,knex,typeorm migration:run);- Rate limiting (
express-rate-limit);- CORS, Helmet, CSRF-защита.
Сравнение менеджеров пакетов: npm, yarn, pnpm
| Критерий | npm | Yarn (Classic & Berry) | pnpm |
|---|---|---|---|
| Разработчик | npm, Inc. → GitHub | Facebook (Classic), община (Berry) | Zoltan Kochan (независимый) |
| Лицензия | Artistic-2.0 | BSD-2-Clause (Classic), MIT (Berry) | MIT |
Модель хранения node_modules | Nested (v2) → Flat (v3+) | Plug’n’Play (Berry), Flat (Classic) | Content-addressable store + symlinked node_modules |
| Скорость установки | ✅ хорошая | ✅ (Berry — очень быстрая) | 🚀 наиболее быстрая (hard links, shared store) |
| Дисковое пространство | ❌ дублирование пакетов | ⚖️ умеренное | ✅ минимальное — один пакет → одна копия в глобальном хранилище (~/.pnpm-store) |
| Поддержка workspace’ов | ✅ (npm 7+) | ✅ (изначально) | ✅ (наиболее стабильная реализация) |
| Файлы блокировки | package-lock.json | yarn.lock | pnpm-lock.yaml (YAML, человекочитаемый) |
| Безопасность | npm audit | yarn audit | pnpm audit, политики разрешений (public-hoist-pattern) |
| Особенности | Интеграция с GitHub Packages, Orgs | Zero-installs (Berry), Constraints | Повышает воспроизводимость, исключает «фantom dependencies» |
Когда что использовать?
- npm — стандарт де-факто. Подходит для большинства проектов, особенно если команда уже знакома с ним. Интеграция с GitHub снижает порог администрирования приватных пакетов.
- yarn (Berry) — если нужна максимальная скорость локальной разработки, zero-installs (артефакты в репозитории), строгий контроль зависимостей.
- pnpm — для больших монорепозиториев, CI/CD-сред с ограничениями по диску, проектов, где важна воспроизводимость сборки. Рекомендован OpenJS Foundation как альтернатива.
Все три совместимы с
package.json. Переключение между ними возможно без потери функциональности — требуется лишь удалитьnode_modulesиlock-файл, затем выполнитьinstallчерез новый менеджер.
Отладка Node.js: VS Code и Chrome DevTools
VS Code (рекомендуемый способ)
- Установите расширение Node.js Debugger (входит в состав по умолчанию).
- В корне проекта создайте
.vscode/launch.json:{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Запустить и отладить",
"skipFiles": ["<node_internals>/**"],
"program": "${workspaceFolder}/src/server.ts",
"preLaunchTask": "tsc: build - tsconfig.json",
"outFiles": ["${workspaceFolder}/dist/**/*.js"]
}
]
} - Установите точки останова (breakpoints) в редакторе.
- Нажмите F5 — запуск с отладкой.
Поддержка:
- Переменные, call stack, watch-выражения;
- Отладка воркеров, дочерних процессов (
"autoAttachChildProcesses": true); - Присоединение к уже запущенному процессу (
"request": "attach").
Chrome DevTools
Node.js поддерживает отладку через Chrome DevTools:
node --inspect-brk src/server.ts
Затем:
- Откройте
chrome://inspect; - Нажмите Open dedicated DevTools for Node;
- Появится интерфейс, идентичный браузерному: Sources, Console, Network (для HTTP-запросов), Memory, Performance.
Преимущества:
- Профилирование памяти (heap snapshots);
- Запись производительности (CPU profiling);
- Интеграция с
console.time()иperformance.mark().
Совет: используйте
--inspect-brk, а не--inspect, чтобы остановиться на первой строке — иначе точки останова могут не успеть загрузиться.
Развёртывание: от Docker до облака
Docker-контейнер
Dockerfile (production-оптимизированный, многоступенчатый):
# Этап 1: сборка
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build # tsc → dist/
# Этап 2: runtime
FROM node:18-alpine
WORKDIR /app
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
COPY package*.json ./
USER node # запуск от непривилегированного пользователя
EXPOSE 3000
CMD ["node", "dist/server.js"]
.dockerignore:
node_modules
.git
.env
Dockerfile
.dockerignore
Сборка и запуск:
docker build -t tasks-api .
docker run -p 3000:3000 --env-file .env.tasks tasks-api
Облачные платформы
| Платформа | Подход | Особенности |
|---|---|---|
| Render | Git-push deployment | Бесплатный tier, PostgreSQL «в комплекте», простота настройки |
| Railway | Config-as-code (railway.toml) | Мгновенный деплой из GitHub, интеграция с базами, переменные окружения в UI |
| AWS Elastic Beanstalk | PaaS для Node.js | Полный контроль над инстансами, autoscaling, ELB, интеграция с RDS |
| Fly.io | Контейнер + глобальное размещение | Автоматический multi-region deploys, WebSockets «из коробки» |
| Vercel / Netlify | Serverless Functions | Для API на основе express + vercel адаптера; cold starts, ограничения по времени |
Рекомендация для старта: Render или Railway — минимальный порог входа, встроенные БД, HTTPS по умолчанию.
Пример render.yaml (для Render):
services:
- type: web
name: tasks-api
env: node
region: frankfurt
plan: free
buildCommand: "npm install && npm run build"
startCommand: "node dist/server.js"
envVars:
- key: DATABASE_URL
fromDatabase:
name: tasks-db
property: connectionString
databases:
- name: tasks-db
databaseName: tasks
user: app
plan: free
Поместите этот файл в корень репозитория — Render автоматически применит конфигурацию при подключении через GitHub.